package cn.jolyoulu.common.mybatis.plus.plugin.page;

import cn.jolyoulu.common.mybatis.plus.plugin.AbstractPlugin;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.scripting.xmltags.DynamicSqlSource;
import org.apache.ibatis.scripting.xmltags.TextSqlNode;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import cn.jolyoulu.common.mybatis.plus.plugin.utils.MybatisPluginUtil;

import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.sql.SQLException;
import java.util.*;

/**
 * @Author: JolyouLu
 * @Date: 2023/3/19 15:01
 * @Description
 */
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
})
@Slf4j
public class PageQueryPlugin extends AbstractPlugin implements Interceptor {

    public PageQueryPlugin() {
        super();
    }

    public PageQueryPlugin(boolean debug) {
        super(debug);
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            //由于逻辑关系，只会进入一次
            if (args.length == 4) {
                //4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            if (Objects.isNull(boundSql)) {
                boundSql = ms.getBoundSql(parameter);
            }
            //是分页查询
            JlPage<?> jlPage = JlPageLocal.get();
            if (Objects.nonNull(jlPage) && !jlPage.getFlag()) {
                return handlerJlPageQuery(invocation,jlPage,executor,ms,parameter,boundSql,rowBounds,resultHandler,cacheKey);
            }
        } catch (Exception e) {
            log.error("", e);
        }
        return invocation.proceed();
    }

    /**
     * 处理分页逻辑
     * @param invocation mybatis拦截器，使用invocation.proceed()执行被拦截的原始方法，返回方法执行结果
     * @param jlPage 分页对象
     * @param executor 执行器
     * @param ms sql映射对象
     * @param parameter 方法参数
     * @param boundSql 已解析的sql信息，包含"?"占位符的sql
     * @param rowBounds 用于内存分页的参数类（非数据库层面的分页）
     * @param resultHandler 自定义的结果处理器，handleResult(ResultContext<?>)处理每一行结果
     * @param cacheKey 缓存key，用于生成缓存的唯一标识
     * @return
     * @throws SQLException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    private Object handlerJlPageQuery(Invocation invocation,
                                      JlPage<?> jlPage,
                                      Executor executor,
                                      MappedStatement ms,
                                      Object parameter,
                                      BoundSql boundSql,
                                      RowBounds rowBounds,
                                      ResultHandler resultHandler,
                                      CacheKey cacheKey) throws SQLException, InvocationTargetException, IllegalAccessException {
        jlPage.setTotal(0L); //默认总数0
        jlPage.setFlag(true);
        if (jlPage.getOptimize()) { //走优化查询
            //生成SQL，pageSize原来基础上+1
            Long back = jlPage.getPageSize();
            jlPage.setPageSize(back + 1);
            String sql = boundSql.getSql();
            String limitSql = gentLimitSql(sql, jlPage);
            BoundSql limitBoundSql = boundSql(ms, limitSql, parameter);
            List<Object> objectList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, limitBoundSql);
            if (Objects.isNull(objectList)){
                return new ArrayList<>();
            }
            if (objectList.size() > back){
                jlPage.setHasNext(true);
                objectList.removeLast(); //删除最后一条
            }else {
                jlPage.setHasNext(false);
            }
            jlPage.setTotal(null);
            jlPage.setPageSize(back); //还原pageSize
            return objectList;
        } else {
            notOptimizeCount(executor, ms, parameter, jlPage);
            if (jlPage.getTotal().equals(0L)){
                return new ArrayList<>();
            }
            //查询末尾追加LIMIT
            String sql = boundSql.getSql();
            String limitSql = gentLimitSql(sql, jlPage);
            BoundSql limitBoundSql = boundSql(ms, limitSql, parameter);
            return executor.query(ms,parameter,rowBounds,resultHandler,cacheKey,limitBoundSql);
        }
    }

    //利用EXPLAIN优化COUNT
    private void optimizeCount(Executor executor, MappedStatement ms, Object parameter, JlPage jlPage) throws SQLException {
        //拼接一条查询sql
        String countSql = "EXPLAIN " + gentCountSql(ms.getBoundSql(parameter).getSql());
        BoundSql countBoundSql = boundSql(ms, countSql, parameter);
        MappedStatement countMs = mappedStatement(ms, countSql);
        log("执行的countSql ==> {}", countSql);
        MybatisPluginUtil.runQuerySql(executor, countMs, parameter, new CacheKey(), countBoundSql, rs -> {
            List<Object> list = rs.getResultList();
            if (Objects.nonNull(list) && !list.isEmpty() && list.get(0) instanceof Map){
                HashMap<String,Object> map = (HashMap<String, Object>) list.get(0);
                if (map.containsKey("rows")){
                    jlPage.setTotal(((BigInteger)map.get("rows")).longValue());
                }
            }
        });
    }

    //非优化的COUNT，默认的分页合计
    private void notOptimizeCount(Executor executor, MappedStatement ms, Object parameter, JlPage jlPage) throws SQLException {
        //拼接一条查询sql
        String countSql = gentCountSql(ms.getBoundSql(parameter).getSql());
        BoundSql countBoundSql = boundSql(ms, countSql, parameter);
        MappedStatement countMs = mappedStatement(ms, countSql);

        log("执行的countSql ==> {}", countSql);
        MybatisPluginUtil.runQuerySql(executor, countMs, parameter, new CacheKey(), countBoundSql, rs -> {
            List<Object> list = rs.getResultList();
            if (Objects.nonNull(list) && !list.isEmpty() && list.get(0) instanceof Map){
                HashMap<String,Object> map = (HashMap<String, Object>) list.get(0);
                if (map.containsKey("count")){
                    jlPage.setTotal((Long) map.get("count"));
                }
            }
        });
    }

    //构建一个BoundSql
    private BoundSql boundSql(MappedStatement ms, String sql, Object parameter) {
        return new BoundSql(ms.getConfiguration(), sql, ms.getBoundSql(parameter).getParameterMappings(), parameter);
    }

    //构建一个MappedStatement
    private MappedStatement mappedStatement(MappedStatement ms, String sql) {
        SqlSource sqlSource = new DynamicSqlSource(ms.getConfiguration(), new TextSqlNode(sql));
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId() + "_COUNT", sqlSource, SqlCommandType.SELECT);
        List<ResultMap> resultMaps = new ArrayList<>();
        resultMaps.add(new ResultMap.Builder(ms.getConfiguration(), ms.getId() + "_COUNT-Inline", Map.class, new ArrayList<>()).build());
        builder.resultMaps(resultMaps);
        return builder.build();
    }

    //获取COUNT SQL
    private String gentCountSql(String sql) {
        return "SELECT COUNT(*) AS count FROM (" + sql + ") tmp";
    }

    //拼接LIMIT SQL
    private String gentLimitSql(String sql, JlPage jlPage) {
        return sql + " LIMIT " + jlPage.getLimitStart() + " , " + jlPage.getPageSize();
    }
}
